#ifndef QRCODECONTROLLER_H
#define QRCODECONTROLLER_H

#include <QObject>

#include <QThread>
#include <QMutex>
#include <QMutexLocker>
#include <QDebug>
#include <QTimer>
#include <QLabel>
#include <QImage>

#include "opencv2/opencv.hpp"
#include "opencv2/highgui.hpp"
#include "opencv2/core.hpp"
#include "opencv2/video.hpp"
#include "opencv2/wechat_qrcode.hpp"
#include "opencv2/calib3d.hpp"


#define USE_QT_MULTIMEDIA
#ifdef USE_QT_MULTIMEDIA
// need 'QT += multimedia' in .pro
#include <QCamera>
#include <QCameraInfo>
#endif // USE_QT_MULTIMEDIA

class QRCodeCameraWorker;
class QRCodeDetectWorker;

// note:
// 该类不会自动断线重联
// 断线,需要手动调用重新连接
class QRCodeController : public QObject
{
    Q_OBJECT
public:
    struct DetectResult {
        std::vector<std::string> strs;
        std::vector<cv::Mat> points;
        cv::Mat image;
        void clear() { strs.clear(); points.clear(); image.release(); }
        bool empty() const {return strs.empty();}
    };

    enum State {
        CAMERA_NOT_OPENED,
        CAMERA_OPENED_NOT_DETECTING,
        CAMERA_DETECTING
    };

    explicit QRCodeController(QString camera_name, QObject *parent = nullptr);
    ~QRCodeController();

    /* Interfaces */
    // CameraName setting
    void setCameraName(QString camera_name);
    const QString& getCameraName() const {return _camera_name;}

    // State Check
    State state() const {return _state;}
    bool isCameraOpened() const {return _state != State::CAMERA_NOT_OPENED;}
    bool isDetecting() const {return _state == State::CAMERA_DETECTING;}

    // note that detection will be stoppen once a result is detected
    // recall this function to relaunch a new detection
    void openCameraAndDetect(bool reopen_forced = false);

    // close the camera device and stop detecting
    void closeCamera();

    // get the current camera image
    // return an empty object if camera is not opened
    cv::Mat getCurrentCameraImageCVMat() const;
    QImage getCurrentCameraImageQImage() const;

    // get the last valid detection result
    // note that the result will be reset if you close the camera
    DetectResult getLastValidDetectedResult() const {return _last_valid_result;}

signals:
    void detected(DetectResult result); // this signal is emitted when a result is detected

private:
    QString _camera_name; // camera device name, e.g. "/dev/video0"

    QRCodeCameraWorker *_camera_worker = nullptr;
    QThread *_camera_worker_thread = nullptr;

    QRCodeDetectWorker *_detect_worker = nullptr;
    QThread *_detect_worker_thread = nullptr;

    DetectResult _last_valid_result;

    State _state = State::CAMERA_NOT_OPENED;

signals:
    void sg_startTimer();
    void sg_stopTimer();

#ifdef USE_QT_MULTIMEDIA
public:
    static decltype (auto) getAvailbleCameras() {
        return QCameraInfo::availableCameras();
    }
#endif // USE_QT_MULTIMEDIA

    static void setQImageToLabel(const QImage& image, QLabel* label) {
        if (!image.isNull())
            label->setPixmap(QPixmap::fromImage(image.scaled(label->size(), Qt::KeepAspectRatio)));
    }

    static bool drawContoursOfDetectResultToLabel(const QRCodeController::DetectResult& result,
                                                  QLabel* label, bool include_text = false) {
        if (result.empty()) return false;
        cv::cvtColor(result.image, result.image, cv::COLOR_BGR2RGB);

        for (std::size_t i = 0; i < result.strs.size(); ++i) {
#define XX(j) \
auto pt##j = cv::Point((int)result.points[i].at<float>(j, 0), (int)result.points[i].at<float>(j, 1))
            XX(0); XX(1); XX(2); XX(3);
#undef XX
            cv::line(result.image, pt0, pt1, {255,0,0}, 2);
            cv::line(result.image, pt1, pt2, {255,0,0}, 2);
            cv::line(result.image, pt2, pt3, {255,0,0}, 2);
            cv::line(result.image, pt3, pt0, {255,0,0}, 2);
            if (include_text)
                cv::putText(result.image, result.strs[i], pt1, 0, 1, {0,255,0}, 2);
        }

        auto q_image = QImage((const unsigned char*)(result.image.data), result.image.cols, result.image.rows, QImage::Format_RGB888);
        label->setPixmap(QPixmap::fromImage(q_image.scaled(label->size(), Qt::KeepAspectRatio)));

        return true;
    }

};

// use mutex for thread sync
class QRCodeCameraWorker : public QObject
{
    Q_OBJECT
public:
    explicit QRCodeCameraWorker(QString camera_name, QObject *parent = nullptr);
    bool isCameraOpened() const {QMutexLocker locker(&_mutex); return _camera_opened;}
    cv::Mat getCurrentCameraImage() const;

    void openCamera();
    void closeCamera() {QMutexLocker locker(&_mutex); _cv_capture.release(); _camera_opened = false;}
    void setCameraName(QString camera_name) {QMutexLocker locker(&_mutex); _camera_name = std::move(camera_name);}

private:
    uint32_t _pixel_width = 1280;
    uint32_t _pixel_height = 720;
    uint32_t _fps = 30;

    mutable cv::VideoCapture _cv_capture;
    QString _camera_name;
    mutable QMutex _mutex;

    mutable bool _camera_opened = false;
};

// use signal - slot for threan sync
class QRCodeDetectWorker : public QObject
{
    Q_OBJECT
public:
    // need to catch exception of this constructor
    explicit QRCodeDetectWorker(QRCodeCameraWorker* camera_worker,
                                std::string model_dir = R"(../QRCodeDemo/resource/opencv_3rdparty-wechat_qrcode/)",
                                int cycle_timeout_msec = 500,
                                double scale_factor = 1.0,
                                QObject *parent = nullptr);

private:
    QRCodeController::DetectResult _detect(cv::Mat mat);

private:
    QRCodeCameraWorker* _camera_worker = nullptr;
    std::shared_ptr<cv::wechat_qrcode::WeChatQRCode> _wechat_qr;

    QTimer* _timer;
    int _timeout_msec = 500; // detect cycle

//    std::string _model_dir = R"(../QRCodeDemo/resource/opencv_3rdparty-wechat_qrcode/)";

signals:
    void detected(QRCodeController::DetectResult result);

public slots:
    void sl_startTimer() {_timer->start(_timeout_msec);}
    void sl_stopTimer() {_timer->stop();}
    void sl_updateDetectTimeout(int timeout_msec) {_timeout_msec = timeout_msec;}
    void sl_updateScaleFactor(double scale_factor) {_wechat_qr->setScaleFactor(scale_factor);}
};


#endif // QRCODECONTROLLER_H
